<script setup>
import { ref, onMounted } from "vue";
// 节点组件
import DemoNode from "../components/DemoNode.vue";
// 流程图框架
import { Graph } from "@antv/x6";
// 自定义节点组件插件
import { register, getTeleport } from "@antv/x6-vue-shape";
// 提示
import { useMessage } from "naive-ui";
// 自动布局插件
import dagre from "dagre";
// 小地图插件
import { MiniMap } from "@antv/x6-plugin-minimap";
// 新增修改节点弹窗
import AddEditFrom from "../components/AddEditFrom.vue";
// 将流程图导出成图片保存到本地
import { Export } from "@antv/x6-plugin-export";

const message = useMessage();
// 流程图框架对象
let graph = null;

// 布局方式
const options = [
  {
    label: "左右布局",
    value: "LR",
  },
  {
    label: "右左布局",
    value: "RL",
  },
  {
    label: "上下布局",
    value: "TB",
  },
  {
    label: "下上布局",
    value: "BT",
  },
];
const dirActive = ref("LR");
// 监听布局方式
const changeDir = (value) => {
  dirActive.value = value;
  layout();
};

// 对齐方式
const alignOptions = [
  {
    label: "居中",
    value: undefined,
  },
  {
    label: "上左",
    value: "UL",
  },
  {
    label: "上右",
    value: "UR",
  },
  {
    label: "下左",
    value: "DL",
  },
  {
    label: "下右",
    value: "DR",
  },
];
const alignActive = ref(undefined);
// 监听对齐方式
const changeAlign = (value) => {
  alignActive.value = value;
  layout();
};

// ranksep:元素节点X间隔 marX
const ranksep = ref("100");
const changeRanksep = (value) => {
  layout();
};

// nodesep:元素节点Y间隔 marY
const nodesep = ref("20");
const changeNodesep = (value) => {
  layout();
};
// 自动布局
function layout() {
  // 布局方向
  // LR：主左  RL：主右 TB：主上 BT：主下
  const dir = dirActive.value; // LR RL TB BT
  // 对齐方式
  const align = alignActive.value;
  // 节点数据
  const nodes = graph.getNodes();
  // 节点片段数据
  const edges = graph.getEdges();
  // 自动布局插件对象
  const g = new dagre.graphlib.Graph();
  // ranksep:元素节点X间隔 marX
  const rank = ranksep.value;
  // nodesep:元素节点Y间隔 marY
  const node = nodesep.value;
  // 设置对齐方式，布局方式，间距
  g.setGraph({ rankdir: dir, align: align, ranksep: rank, nodesep: node });
  g.setDefaultEdgeLabel(() => ({}));

  // 设定节点宽高，自动设置布局
  const width = 300;
  let height = 130;
  nodes.forEach((node) => {
    // 有没有子节点，计算新高度
    if (node.data.taskArr.length != 0) {
      // 子节点有没有被收起
      if (node.data.closed) {
        g.setNode(node.id, {
          width,
          height: height + node.data.taskArr.length * 90,
        });
      } else {
        g.setNode(node.id, { width, height });
      }
    } else {
      g.setNode(node.id, { width, height });
    }
  });

  // 根据上面新设置的节点，重新设置布局节点片段
  edges.forEach((edge) => {
    const source = edge.getSource();
    const target = edge.getTarget();
    g.setEdge(source.cell, target.cell);
  });

  // 重新布局
  dagre.layout(g);

  g.nodes().forEach((id) => {
    const node = graph.getCellById(id);
    if (node) {
      const pos = g.node(id);
      // 重新布局，设置位置
      node.position(pos.x, pos.y);
    }
  });

  edges.forEach((edge) => {
    const source = edge.getSourceNode();
    const target = edge.getTargetNode();
    const sourceBBox = source.getBBox();
    const targetBBox = target.getBBox();

    // 根据布局方式排列位置
    if ((dir === "LR" || dir === "RL") && sourceBBox.y !== targetBBox.y) {
      const gap =
        dir === "LR"
          ? targetBBox.x - sourceBBox.x - sourceBBox.width
          : -sourceBBox.x + targetBBox.x + targetBBox.width;
      const fix = dir === "LR" ? sourceBBox.width : 0;
      const x = sourceBBox.x + fix + gap / 2;
      edge.setVertices([
        { x, y: sourceBBox.center.y },
        { x, y: targetBBox.center.y },
      ]);
    } else if (
      (dir === "TB" || dir === "BT") &&
      sourceBBox.x !== targetBBox.x
    ) {
      const gap =
        dir === "TB"
          ? targetBBox.y - sourceBBox.y - sourceBBox.height
          : -sourceBBox.y + targetBBox.y + targetBBox.height;
      const fix = dir === "TB" ? sourceBBox.height : 0;
      const y = sourceBBox.y + fix + gap / 2;
      edge.setVertices([
        { x: sourceBBox.center.x, y },
        { x: targetBBox.center.x, y },
      ]);
    } else {
      edge.setVertices([]);
    }
  });

  // 重新渲染小地图 先移除，在新增
  graph.disposePlugins("minimap");
  graph.use(
    new MiniMap({
      container: document.getElementById("minimap"),
      width: 400,
      height: 200,
      padding: 20,
    })
  );

  // 挂载导出流程图片插件  重新渲染图片元素 先移除，在新增
  graph.disposePlugins("export");
  graph.use(new Export());
}
// 挂载组件
register({
  // 节点名称
  shape: "custom-vue-node",
  width: 300,
  height: 130,
  // 组件
  component: DemoNode,
});
// 节点组件 在页面渲染
const TeleportContainer = getTeleport();
// 工作流数据
let addNodeArr = ref([]);

// 增加节点
const add = (data) => {
  if (data.parentId) {
    // 如果新增子节点，则默认展开父节点
    addNodeArr.value.forEach((item) => {
      if (item.id == data.parentId) {
        item.data.nodeClosed = true;
      }
    });
  }
  const id = new Date().getTime();
  addNodeArr.value.push({
    id,
    shape: "custom-vue-node",
    x: 0,
    y: 0,
    data,
  });
  // 重新生成节点
  addNodes(addNodeArr.value);
  message.success("添加成功");
};

//修改节点
const update = (id, data) => {
  // 改数组
  addNodeArr.value.forEach((item) => {
    if (item.id == id) {
      //新增子任务 => 修改节点后打开子任务
      if (data.taskArr.length != item.data.taskArr.length) {
        data.closed = true;
      }
      item.data = data;
    }
  });
  // 重新生成节点
  addNodes(addNodeArr.value);
  message.success("修改成功");
};

// 删除节点
const del = (id) => {
  // 删除节点，节点下的子节点继承删除节点的父节点id
  // const parentObj = addNodeArr.value.find(items => items.id == id)
  // addNodeArr.value.forEach(item => {
  //   if (item.data.parentId == id) {
  //     item.data.parentId = parentObj.data.parentId
  //   }
  // })
  // addNodeArr.value = addNodeArr.value.filter(item => item.id !== id)

  // 删除节点，节点下的子任务全部删除
  let deIdlArr = [id];
  function delNode(ids) {
    addNodeArr.value.forEach((item) => {
      if (item.data.parentId == ids) {
        deIdlArr.push(item.id);
        delNode(item.id);
      }
    });
  }
  delNode(id);

  // 过滤删除节点
  addNodeArr.value = addNodeArr.value.filter((item) => {
    return !deIdlArr.includes(item.id);
  });
  // 重新生成节点
  addNodes(addNodeArr.value);
  message.success("删除成功");
};

// 展开收起子任务
const expandNode = (id) => {
  let msg = "";
  addNodeArr.value.forEach((item) => {
    if (item.id == id) {
      item.data.closed = !item.data.closed;
      msg = item.data.closed ? "展开" : "收起";
    }
  });
  addNodes(addNodeArr.value);
  message.success(msg + "成功");
};

// 展开收起子节点
const expandNodeChild = (id) => {
  let msg = "";
  addNodeArr.value.forEach((item) => {
    if (item.id == id) {
      item.data.nodeClosed = !item.data.nodeClosed;
      msg = item.data.nodeClosed ? "展开" : "收起";
    }
  });
  addNodes(addNodeArr.value);
  message.success(msg + "成功");
};

// 遍历添加节点
const addNodes = (arr) => {
  // 先清空节点在重新渲染
  graph.fromJSON({
    cells: [],
  });

  // 根据创建时间从旧到新排序
  arr.sort((a, b) => {
    return new Date(a.data.time).getTime() - new Date(b.data.time).getTime();
  });

  // 遍历创建节点片段
  // 主节点
  let source = null;
  // 子节点
  let target = null;
  arr.forEach((item) => {
    // 判断是不是根节点
    if (item.data.parentId) {
      // 获取父节点
      const parent = arr.find((items) => items.id === item.data.parentId);
      // 如果收起子节点，子节点不渲染以及子节点下所有节点
      if (parent.data.nodeClosed) {
        if (parent) {
          // 根据数据创建节点
          source = graph.addNode(parent);
          target = graph.addNode(item);

          // 创建片段
          graph.addEdge({
            source: {
              cell: source,
              // 边连接点
              anchor: {
                // 链接位置
                name: "right",
                // 边连接点位置
                args: {
                  dy: 0,
                  dx: 44,
                },
              },
            },
            target: {
              cell: target,
              anchor: {
                // 链接位置
                name: "left",
              },
            },
            // 圆角连接器，将起点、路由点、终点通过直线按顺序连接，并在线段连接处通过圆弧连接（倒圆角）
            connector: {
              name: "rounded",
              args: {
                radius: 100,
              },
            },
            attrs: {
              // 元素连接线样式
              line: {
                // 颜色
                stroke: "#c7c8cc",
                // 宽度
                strokeWidth: 1,
              },
            },
          });
        } else {
          // 数据错误
          message.error("父节点不存在,请检查数据");
        }
      } else {
        // 父节点收起节点，子节点全部收起
        item.data.nodeClosed = false;
      }
    } else {
      // 创建主节点
      source = graph.addNode(item);
    }
  });
  // 自动布局
  layout();
  // 保存
  save();
};

// 清除流程图缓存
const closes = () => {
  localStorage.removeItem("addNodeArr");
  graph.fromJSON({
    cells: [],
  });
  addNodeArr.value = [];
  message.success("清除成功");
};

// 读取流程图
const saves = () => {
  let addNodeArrJson = localStorage.getItem("addNodeArr");
  if (addNodeArrJson) {
    addNodeArrJson = JSON.parse(addNodeArrJson);
    // 由于转json后，方法字段会变成空值，需要重新绑定方法
    addNodeArr.value = addNodeArrJson.map((item) => {
      // 新增
      item.data.add = add;
      // 删除
      item.data.del = del;
      // 修改
      item.data.update = update;
      // 子任务收起
      item.data.expandNode = expandNode;
      // 子节点收起
      item.data.expandNodeChild = expandNodeChild;
      return item;
    });
  } else {
    // 清空
    addNodeArr.value = [];
  }
  // 重新生成节点
  addNodes(addNodeArr.value);
};

// 保存流程图
const save = () => {
  localStorage.setItem("addNodeArr", JSON.stringify(addNodeArr.value));
};

// 导出png图片功能
const exportModal = ref(false);
const exportObj = ref({
  WHtype: 1, //分辨率 1、高清 2、超清 3、2k，4、4k 5、8k ，6,16k
  type: 1, //导出类型1、本地导出，2、上传保存，然后再本地导出
  format: 1, //导出格式1、png，2、svg
  name: "",
  width: "",
  height: "",
});
const exportArr = ref([
  { label: "高清画质", value: 1 },
  { label: "超清画质", value: 2 },
  { label: "2K画质", value: 3 },
  { label: "4K画质", value: 4 },
  { label: "8K画质", value: 5 },
  { label: "16K画质", value: 6 },
]);
// 分辨率自定义 1、额定 2、自定义
const WHtypeType = ref(1);
const typeArr = ref([
  { label: "本地导出,不上传", value: 1 },
  { label: "本地导出,上传保存", value: 2 },
]);
const formatArr = ref([
  { label: ".png格式", value: 1 },
  { label: ".svg格式", value: 2 },
]);
// 取消导出
const exportClose = () => {
  exportObj.value = {
    WHtype: 1,
    type: 1,
    format: 1,
    name: "",
    width: "",
    height: "",
  };
  exportModal.value = false;
};
// 确认导出
const exportPng = (type) => {
  if (type == 1) {
    exportModal.value = true;
  } else {
    let width = 1280;
    let height = 720;
    if (WHtypeType.value == 1) {
      switch (exportObj.value.WHtype) {
        case 1:
          // 高清
          width = 1280;
          height = 720;
          break;
        case 2:
          // 超清
          width = 1920;
          height = 1080;
          break;
        case 3:
          // 2k
          width = 2560;
          height = 1440;
          break;
        case 4:
          // 4k
          width = 4096;
          height = 2160;
          break;
        case 5:
          //8k
          width = 7680;
          height = 4320;
          break;
        case 6:
          //16k
          width = 15260;
          height = 8640;
          break;
        default:
          message.error("暂不支持该分辨率");
          break;
      }
    } else {
      if (exportObj.value.width > 15999 || exportObj.value.height > 15999) {
        message.error("分辨率过大");
        return;
      }
      width = Number(exportObj.value.width);
      height = Number(exportObj.value.height);
    }

    // 导出图片配置
    const option = {
      width: width,
      height: height,
      backgroundColor: "#f7f8fa",
      padding: 20,
      // 图片质量，可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围，将会使用默认值 0.92
      quality: 1,
      // 允许外部样式引入，否则框架组件没用样式，节点组件采用行内样式不会丢失样式
      copyStyles: true,
      // 部分框架样式丢失，通过此属性引入样式，下面是进度条组件样式丢失引入
      stylesheet: `
          .n-progress.n-progress--line .n-progress-content{
            display: flex;
            align-items: center;
          }
          .n-progress .n-progress-graph .n-progress-graph-line .n-progress-graph-line-rail{
            position: relative;
            overflow: hidden;
            height: var(--n-rail-height);
            border-radius: 5px;
            background-color: var(--n-rail-color);
            transition: background-color .3s var(--n-bezier);
          }
        `,
    };

    if (exportObj.value.type == 1) {
      console.log("本地图片导出");
      if (exportObj.value.format == 1) {
        // 本地图片导出
        graph.exportPNG(exportObj.value.name, option);
      } else {
        graph.exportSVG(exportObj.value.name, option);
      }
    } else {
      console.log("本地图片导出,上传base64数据");
      // base64格式导出下载图片
      let exportName = exportObj.value.name;
      if (exportObj.value.format == 1) {
        graph.toPNG((base64Data) => {
          function downloadBase64Image() {
            const a = document.createElement("a");
            a.href = base64Data;
            a.download = exportName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
          }
          downloadBase64Image();
        }, option);
      } else {
        graph.toSVG((svgContent) => {
          const svgBlob = new Blob([svgContent], { type: "image/svg+xml" });
          const svgUrl = URL.createObjectURL(svgBlob);

          const a = document.createElement("a");
          a.href = svgUrl;
          a.download = exportName + ".svg";
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);

          URL.revokeObjectURL(svgUrl);
        }, option);
      }
    }

    exportClose();
  }
};

// 挂载流程图元素
onMounted(() => {
  // 获取流程图元素
  const container = document.getElementById("container");
  // 设置全屏宽高
  let width = document.documentElement.clientWidth;
  let height = document.documentElement.clientHeight;
  graph = new Graph({
    container,
    width,
    height,
    // 背景颜色
    background: {
      color: "#f7f8fa",
    },
    autoResize: true, //大小适应
    panning: true, //缩放
    mousewheel: true, //平移
    // 配置交互
    interacting: {
      nodeMovable: false, //节点是否可以被移动
      edgeMovable: false, //边是否可以被移动
    },
    // 连接配置
    connecting: {
      allowLoop: false, //是否允许创建循环连线
      allowMulti: false, //是否允许在相同的起始节点和终止之间创建多条边
    },
  });

  // 读取流程图
  saves();
});

// 打开新增弹窗
const showModal = ref(false);
// 新增弹窗Dom元素
const addNodeDom = ref(null);
const openAdd = () => {
  showModal.value = true;

  setTimeout(() => {
    // 引用子组件方法更新表单数据
    addNodeDom.value.updateForm();
  }, 10);
};

// 关闭弹窗
const addClose = () => {
  showModal.value = false;
};

// 确认保存
const addOpen = (formData) => {
  formData.add = add;
  formData.del = del;
  formData.update = update;
  formData.expandNode = expandNode;
  formData.expandNodeChild = expandNodeChild;
  add(formData);
};
</script>

<template>
  <div>
    <div>
      <!-- 流程图 -->
      <div id="container"></div>

      <!-- 小地图 -->
      <div
        id="minimap"
        style="position: absolute; bottom: 10px; right: 30px"
      ></div>
      <!-- 子节点组件 -->
      <TeleportContainer />
    </div>

    <!-- 设置按钮 -->
    <div>
      <n-button
        type="primary"
        @click="exportPng(1)"
        style="position: absolute; top: 10px; left: 20px"
        >导出图片</n-button
      >
      <n-button
        type="primary"
        @click="openAdd"
        style="position: absolute; top: 10px; left: 120px"
        >新建</n-button
      >
      <n-button
        type="primary"
        @click="closes"
        style="position: absolute; top: 10px; left: 185px"
        >清除</n-button
      >
      <div
        style="
          position: absolute;
          top: 0px;
          left: 250px;
          display: flex;
          background-color: white;
          padding: 10px;
          width: 250px;
          align-items: center;
          border-radius: 5px;
        "
      >
        <div style="width: 120px">布局方式：</div>
        <n-select
          v-model:value="dirActive"
          :on-update:value="changeDir"
          :multiple="false"
          :options="options"
          clearable
        />
      </div>
      <div
        style="
          position: absolute;
          top: 0px;
          left: 510px;
          display: flex;
          background-color: white;
          padding: 10px;
          width: 250px;
          align-items: center;
          border-radius: 5px;
        "
      >
        <div style="width: 120px">对齐方式：</div>
        <n-select
          placeholder="默认居中"
          v-model:value="alignActive"
          :on-update:value="changeAlign"
          :multiple="false"
          :options="alignOptions"
          clearable
        />
      </div>
      <div
        style="
          padding: 10px;
          position: absolute;
          top: 0px;
          left: 770px;
          display: flex;
          align-items: center;
          background-color: white;
        "
      >
        <div
          style="
            display: flex;
            width: 200px;
            align-items: center;
            margin-right: 10px;
          "
        >
          <span style="width: 80px">Y间距：</span>
          <n-input
            type="number"
            v-model:value="nodesep"
            @input="changeNodesep"
            placeholder="请输入节点元素之间的间隔"
          />
        </div>
        <div style="display: flex; width: 200px; align-items: center">
          <span style="width: 80px">X间距：</span>
          <n-input
            type="number"
            v-model:value="ranksep"
            @input="changeRanksep"
            placeholder="请输入层级之间的间隔"
          />
        </div>
      </div>
    </div>

    <!-- 新建弹窗 -->
    <AddEditFrom
      ref="addNodeDom"
      :type="1"
      :showModal="showModal"
      @addClose="addClose"
      @addOpen="addOpen"
    />

    <!-- 导出图片选中分辨率弹窗 -->
    <n-modal :show="exportModal" @maskClick="exportClose">
      <div
        style="
          width: 500px;
          padding: 20px;
          display: flex;
          flex-direction: column;
          background-color: white;
          border-radius: 10px;
        "
      >
        <div style="display: flex; align-items: center">
          <span style="margin-right: 50px; font-size: 18px; font-weight: bold"
            >导出配置</span
          >
          <n-button type="primary" :text="true" @click="exportClose"
            >关闭</n-button
          >
        </div>
        <div style="margin-top: 20px">
          <div style="width: 120px; font-size: 16px; font-weight: 600">
            文件名：
          </div>
          <n-input v-model:value="exportObj.name" placeholder="请输入文件名" />
        </div>
        <div style="margin-top: 20px">
          <div style="width: 120px; font-size: 16px; font-weight: 600">
            分辨率：
          </div>
          <div style="margin: 10px 0">
            <n-button @click="WHtypeType = 1" style="margin-right: 10px"
              >额定分辨率</n-button
            >
            <n-button @click="WHtypeType = 2">自定义</n-button>
          </div>
          <n-select
            v-if="WHtypeType == 1"
            placeholder="默认高清"
            v-model:value="exportObj.WHtype"
            :multiple="false"
            :options="exportArr"
            clearable
          />
          <div
            v-if="WHtypeType == 2"
            style="display: flex; align-items: center"
          >
            <n-input
              type="number"
              v-model:value="exportObj.width"
              placeholder="请输入宽度"
              style="width: 200px; margin-right: 10px"
            />
            <n-input
              type="number"
              v-model:value="exportObj.height"
              placeholder="请输入高度"
              style="width: 200px"
            />
          </div>
        </div>
        <div style="margin-top: 20px">
          <div style="width: 120px; font-size: 16px; font-weight: 600">
            是否上传：
          </div>
          <n-select
            placeholder="默认不上传"
            v-model:value="exportObj.type"
            :multiple="false"
            :options="typeArr"
            clearable
          />
        </div>
        <div style="margin-top: 20px">
          <div style="width: 120px; font-size: 16px; font-weight: 600">
            导出文件格式：
          </div>
          <n-select
            placeholder="默认png"
            v-model:value="exportObj.format"
            :multiple="false"
            :options="formatArr"
            clearable
          />
        </div>

        <div>
          <div style="margin-top: 20px; display: flex; justify-content: center">
            <n-button
              type="primary"
              @click="exportClose"
              style="margin-left: 10px"
              >取消</n-button
            >
            <n-button
              type="primary"
              @click="exportPng(2)"
              style="margin-left: 10px"
              >确认</n-button
            >
          </div>
        </div>
      </div>
    </n-modal>
  </div>
</template>

<style lang="scss">
#app {
  width: 100%;
  height: 100vh !important;
}
</style>
